1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
|
import { Suspense } from "react"
import { Shell } from "@/components/shell"
import { DataTableSkeleton } from "@/components/data-table/data-table-skeleton"
import {
getBiddings,
getBiddingStatusCounts,
getBiddingTypeCounts,
getBiddingManagerCounts,
getBiddingMonthlyStats,
getUserCodeByEmail,
} from "@/lib/bidding/service"
import { searchParamsCache } from "@/lib/bidding/validation"
import { BiddingsPageHeader } from "@/lib/bidding/list/biddings-page-header"
import { BiddingsStatsCards } from "@/lib/bidding/list/biddings-stats-cards"
import { BiddingsTable } from "@/lib/bidding/list/biddings-table"
import { getValidFilters } from "@/lib/data-table"
import { type SearchParams } from "@/types/table"
export const metadata = {
title: "입찰 목록",
description: "입찰 공고를 생성하고 진행 상황을 관리할 수 있습니다.",
}
interface IndexPageProps {
searchParams: Promise<SearchParams>
}
export default async function BiddingsPage(props: IndexPageProps) {
// ✅ nuqs searchParamsCache로 파싱 (타입 안전성 보장)
const searchParams = await props.searchParams
const search = searchParamsCache.parse(searchParams)
const validFilters = getValidFilters(search.filters)
// ✅ 입찰 데이터를 먼저 가져옴
const biddingsResult = await getBiddings({
...search,
filters: validFilters,
})
// ✅ 입찰 데이터에 managerCode 추가
const biddingsDataWithManagerCode = await Promise.all(
biddingsResult.data.map(async (item) => {
let managerCode: string | null = null
if (item.managerEmail) {
managerCode = await getUserCodeByEmail(item.managerEmail)
}
return { ...item, managerCode: managerCode || null }
})
)
// ✅ 모든 데이터를 병렬로 로드
const promises = Promise.all([
Promise.resolve({ ...biddingsResult, data: biddingsDataWithManagerCode }),
getBiddingStatusCounts(),
getBiddingTypeCounts(),
getBiddingManagerCounts(),
getBiddingMonthlyStats(),
])
return (
<Shell className="gap-4">
{/* ═══════════════════════════════════════════════════════════════ */}
{/* 페이지 헤더 */}
{/* ═══════════════════════════════════════════════════════════════ */}
<BiddingsPageHeader />
{/* ═══════════════════════════════════════════════════════════════ */}
{/* 통계 카드들 */}
{/* ═══════════════════════════════════════════════════════════════ */}
<Suspense fallback={<BiddingsStatsCardsSkeleton />}>
<BiddingsStatsCardsWrapper promises={promises} />
</Suspense>
{/* ═══════════════════════════════════════════════════════════════ */}
{/* 메인 테이블 */}
{/* ═══════════════════════════════════════════════════════════════ */}
<Suspense
fallback={
<DataTableSkeleton
columnCount={20}
searchableColumnCount={3}
filterableColumnCount={4}
cellWidths={["10rem", "8rem", "12rem", "15rem", "10rem", "8rem"]}
shrinkZero
/>
}
>
<BiddingsTable promises={promises} />
</Suspense>
</Shell>
)
}
// ═══════════════════════════════════════════════════════════════
// 통계 카드 래퍼 컴포넌트
// ═══════════════════════════════════════════════════════════════
async function BiddingsStatsCardsWrapper({
promises
}: {
promises: Promise<[
Awaited<ReturnType<typeof getBiddings>>,
Awaited<ReturnType<typeof getBiddingStatusCounts>>,
Awaited<ReturnType<typeof getBiddingTypeCounts>>,
Awaited<ReturnType<typeof getBiddingManagerCounts>>,
Awaited<ReturnType<typeof getBiddingMonthlyStats>>,
]>
}) {
const [biddingsResult, statusCounts, typeCounts, managerCounts, monthlyStats] = await promises
return (
<BiddingsStatsCards
total={biddingsResult.total}
statusCounts={statusCounts}
typeCounts={typeCounts}
managerCounts={managerCounts}
monthlyStats={monthlyStats}
/>
)
}
// 통계 카드 스켈레톤
function BiddingsStatsCardsSkeleton() {
return (
<div className="grid gap-4 md:grid-cols-2 lg:grid-cols-4">
{Array.from({ length: 4 }).map((_, i) => (
<div key={i} className="rounded-lg border p-6">
<div className="h-4 bg-muted rounded animate-pulse mb-2" />
<div className="h-8 bg-muted rounded animate-pulse" />
</div>
))}
</div>
)
}
|